<?php
/***********************************************************************
 * @package    IvanEngine
 * @subpackage Core
 * @author     Ivan <ivanzx@msn.com>
 * @created    2010-05-12
 ***********************************************************************/
if(defined("IVANENGINE_CORE_FINDER"))
	die("Redefinition macro in :".__FILE__."(".__LINE__.")");
define("IVANENGINE_CORE_FINDER", true);

require_once (dirname(__FILE__).'/Common.php');

class CFinder extends CObject 
{
	const FT_File		= 'file';
	const FT_Dir		= 'dir';
	const FT_Any		= 'any';
    
	const ST_None		= false;
	const ST_Name		= 'name';
	const ST_Type		= 'type';
	const ST_Size		= 'size';
    
    const RT_Normal     = 0;
    const RT_File       = 1;
    const RT_FileName   = 2;
    const RT_Dir        = 3;
    const RT_Relative   = 4;
    
    const RF_Normal     = 0;
    const RF_Lower      = 1;
    const RF_Upper      = 2;
    const RF_UCFirst    = 3;
	
	protected $m_type					= self::FT_File;
	protected $m_maxDepth				= 1000000;
	protected $m_minDepth				= 0;
	protected $m_rules          		= array();
	protected $m_sortType       		= self::ST_None;
	protected $m_ignoreDir              = array();
	protected $m_ignoreFile             = array();
	protected $m_execs                  = array();
	protected $m_sizes                  = array();
	protected $m_returnType             = self::RT_Normal;
    protected $m_returnFormat           = self::RF_Normal;
	protected $m_isInclideEngineDir     = false;
	
	function __construct()
	{
		parent::__construct();
	}
	
	function __destruct()
	{
		parent::__destruct();
	}
	
	static function Create($type)
	{
		$finder = new CFinder();
        $finder->SetType($type);
		return $finder;
	}
	
	function SetType($type)
	{
		if ($type == self::FT_Any || $type == self::FT_File || $type == self::FT_Dir)
	    {
	    	$this->m_type	= $type;
	    }else{
            throw new CExceptionInvalidArgument('CFinder::Type only is Any/File/Dir');
        }
	    
	   	return $this;
	}
	
	function GetType()
	{
		return $this->m_type;
	}
	
	function MinDepth($depth)
	{
		$this->m_minDepth	= $depth;
		return $this;
	}
	
	function MaxDepth($depth)
	{
		$this->m_maxDepth	= $depth;
		return $this;
	}
	
	/**
	* Adds rules that files must match.
	*
	* You can use patterns (delimited with / sign), globs or simple strings.
	*
	* $finder->AddRule('*.php')
	* $finder->AddRule('/\.php$/') // same as above
	* $finder->AddRule('test.php')
	*
	* @param  list   a list of patterns
	* @return object current CFinder object
	*/
	function AddRule()
	{
		$args = func_get_args();
		$this->m_rules = array_merge($this->m_rules, $this->_ArgsToArray($args));

		return $this;
	}
	
	function AddAntiRule()
	{
		$args = func_get_args();
		$this->m_rules = array_merge($this->m_rules, $this->_ArgsToArray($args, true));

		return $this;
	}
	
	/**
   	* Adds tests for file sizes.
   	*
   	* $finder->AddSizeRule('> 10K');
   	* $finder->AddSizeRule('<= 1Ki');
   	* $finder->AddSizeRule(4);
   	*
   	* @param  list   a list of comparison strings
   	* @return object current sfFinder object
   	*/
  	function AddSizeRule()
  	{
    	$args = func_get_args();
    	for ($i = 0; $i < count($args); $i++)
    	{
      		$this->m_sizes[] = new CNumberCompare($args[$i]);
    	}

    	return $this;
  	}
  	
  	function Sort($type)
  	{
  		$this->m_sortType	= $type;
  		return $this;	
  	}
  	
    function ReturnType($type = self::RT_Normal)
    {
        $this->m_returnType = $type;
        return $this;
    }
      
  	function ReturnFormat($format = self::RF_Normal)
  	{
  		$this->m_returnFormat = $format;
    	return $this;
  	}
  	
  	function IncludeEngineDir($boolen = true)
  	{
  		$this->m_isInclideEngineDir 	= $boolen;
    	return $this;
  	}
  	
	/**
   	* Traverses no further.
   	*
   	* @param  list   a list of patterns, globs to match
   	* @return object current sfFinder object
   	*/
  	function IgnoreDir()
  	{
    	$args = func_get_args();
    	$this->m_ignoreDir = array_merge($this->m_ignoreDir, $this->_ArgsToArray($args));

    	return $this;
  	}

  	/**
   	* IgnoreFile elements that matches.
   	*
   	* @param  list   a list of patterns, globs to match
   	* @return object current sfFinder object
   	*/
  	function IgnoreFile()
  	{
    	$args = func_get_args();
    	$this->m_ignoreFile = array_merge($this->m_ignoreFile, $this->_ArgsToArray($args));

    	return $this;
  	}
  	
	/**
	* Executes function or method for each element.
	*
	* Element match if functino or method returns true.
	*
	* $finder->exec('myfunction');
	* $finder->exec(array($object, 'mymethod'));
	*
	* @param  mixed  function or method to call
	* @return object current CFinder object
	*/
	public function Exec()
	{
		$args = func_get_args();
		for ($i = 0; $i < count($args); $i++)
		{
		  	if (is_array($args[$i]) && !method_exists($args[$i][0], $args[$i][1]))
		  	{
				throw new CException(sprintf('method "%s" does not exist for object "%s".', $args[$i][1], $args[$i][0]));
		 	}
		  	else if (!is_array($args[$i]) && !function_exists($args[$i]))
		  	{
				throw new CException(sprintf('function "%s" does not exist.', $args[$i]));
		  	}
	
		  $this->m_execs[] = $args[$i];
		}
	
		return $this;
	}
	
  	/**
   	* Searches files and directories which match defined rules.
   	*
   	* @return array list of files and directories
   	*/
  	function Go()
  	{
  		$files    = array();
	    $here_dir = getcwd();
	
	    $finder = clone $this;

	    $ignores = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg');
	    $finder->IgnoreFile($ignores)->IgnoreDir($ignores);
	
	    // first argument is an array?
	    $numargs  = func_num_args();
	    $arg_list = func_get_args();
	    if ($numargs == 1 && is_array($arg_list[0]))
	    {
	      	$arg_list = $arg_list[0];
	      	$numargs  = count($arg_list);
	    }
	
	    for ($i = 0; $i < $numargs; $i++)
	    {
	      	$dir = realpath($arg_list[$i]);
	
	      	if (!is_dir($dir))
	      	{
	        	continue;
	      	}
	
            $dir = CFileHelper::CorrectDS($dir);
            // absolute path?
            if (!CFileHelper::IsAbsolutePath($dir))
            {
                $dir = $here_dir.M_DS.$dir;
            }
            $newfiles = CFileHelper::CorrectDS($finder->_SearchIn($dir));
            foreach( $newfiles as $newfile )
            {
                $newfile    = realpath($newfile);
                if( $this->m_returnType != self::RT_Normal )
                {
                    switch($this->m_returnType)
                    {
                        case self::RT_File:
                            $newfile = CFileHelper::GetFileFromPath($newfile);
                            break;
                        case self::RT_FileName:
                            $newfile = CFileHelper::GetFileFromPath($newfile);
                            $pos     = CStringHelper::FindLastOf('.', $newfile);
                            if( $pos != CString::npos ){
                                $newfile = substr($newfile, 0, $pos);
                            }
                            break;
                        case self::RT_Dir:
                            $newfile = CFileHelper::GetDirFromPath($newfile);
                            break;
                        case self::RT_Relative:
                            $newfile = CFileHelper::AToR($newfile);
                            break;
                    }
                }
                if( $this->m_returnFormat != self::RF_Normal )
                {
                    switch($this->m_returnFormat)
                    {
                        case self::RF_Lower:
                            $newfile = CStringHelper::ToLower($newfile);
                            break;
                        case self::RF_Upper:
                            $newfile = CStringHelper::ToUpper($newfile);
                            break;
                        case self::RF_UCFirst:
                            $newfile = ucfirst($newfile);
                            break;
                    }
                }

                array_push($files, $newfile);
            }
	    }
	
	    if ($this->m_sortType == self::ST_Name)
	    {
	      	Sort($files);
	    }
	
	    return array_unique($files);
  	}
  	
	protected function _SearchIn($dir, $depth = 0)
	{
		if ($depth >= $this->m_maxDepth)
		{
	  		return array();
		}

		$dir = realpath($dir);

		if ((!$this->m_isInclideEngineDir) && is_link($dir))
		{
	  		return array();
		}

		$files 			= array();
		$temp_files 	= array();
		$temp_folders 	= array();
		if (is_dir($dir))
		{
	  		$current_dir = opendir($dir);
	  		while (false !== $entryname = readdir($current_dir))
	  		{
				if ($entryname == '.' || $entryname == '..') continue;
		
				$current_entry = $dir.DIRECTORY_SEPARATOR.$entryname;
				if ((!$this->m_isInclideEngineDir) && is_link($current_entry))
				{
				  	continue;
				}
		
				if (is_dir($current_entry))
				{
				  	if ($this->m_sortType == self::ST_Type)
				  	{
						$temp_folders[$entryname] = $current_entry;
				  	}
				  	else
				  	{
						if (($this->m_type == self::FT_Dir || $this->m_type == self::FT_Any) && ($depth >= $this->m_minDepth) && !$this->_IsDiscarded($dir, $entryname) && $this->_MatchNames($dir, $entryname) && $this->_ExecOk($dir, $entryname))
						{
					  		$files[] = $current_entry;
						}
		
						if (!$this->_IsPruned($dir, $entryname))
						{
					  		$files = array_merge($files, $this->_SearchIn($current_entry, $depth + 1));
						}
				  	}
				}
				else
				{
				  	if (($this->m_type != self::FT_Dir || $this->m_type == self::FT_Any) && ($depth >= $this->m_minDepth) && !$this->_IsDiscarded($dir, $entryname) && $this->_MatchNames($dir, $entryname) && $this->_SizeOk($dir, $entryname) && $this->_ExecOk($dir, $entryname))
				  	{
						if ($this->m_sortType == self::ST_Type)
						{
						  $temp_files[] = $current_entry;
						}
						else
						{
						  $files[] = $current_entry;
						}
				  	}
				}
	  		}

		  	if ($this->m_sortType == self::ST_Type)
		  	{
				ksort($temp_folders);
				foreach($temp_folders as $entryname => $current_entry)
				{
			  		if (($this->m_type == self::FT_Dir || $this->m_type == self::FT_Any) && ($depth >= $this->m_minDepth) && !$this->_IsDiscarded($dir, $entryname) && $this->_MatchNames($dir, $entryname) && $this->_ExecOk($dir, $entryname))
			  		{
						$files[] = $current_entry;
			  		}
	
				  	if (!$this->_IsPruned($dir, $entryname))
				  	{
						$files = array_merge($files, $this->_SearchIn($current_entry, $depth + 1));
				  	}
				}
	
				sort($temp_files);
				$files = array_merge($files, $temp_files);
		  	}

	  		closedir($current_dir);
		}

		return $files;
	}

	protected function _MatchNames($dir, $entry)
	{
		if (!count($this->m_rules)) return true;

		// we must match one "not_name" rules to be ko
		$one_not_name_rule = false;
		foreach ($this->m_rules as $args)
		{
	  		list($not, $regex) = $args;
	  		if ($not)
	  		{
				$one_not_name_rule = true;
				if (preg_match($regex, $entry))
				{
				  	return false;
				}
	  		}
		}

		$one_name_rule = false;
		// we must match one "name" rules to be ok
		foreach ($this->m_rules as $args)
		{
	  		list($not, $regex) = $args;
	  		if (!$not)
	  		{
				$one_name_rule = true;
				if (preg_match($regex, $entry))
				{
			  		return true;
				}
	  		}
		}

		if ($one_not_name_rule && $one_name_rule)
		{
	  		return false;
		}
		else if ($one_not_name_rule)
		{
		  	return true;
		}
		else if ($one_name_rule)
		{
		  	return false;
		}
		else
		{
		  	return true;
		}
	}
	
	protected function _SizeOk($dir, $entry)
	{
		if (!count($this->m_sizes)) return true;
	
		if (!is_file($dir.DIRECTORY_SEPARATOR.$entry)) return true;
	
		$filesize = filesize($dir.DIRECTORY_SEPARATOR.$entry);
		foreach ($this->m_sizes as $number_compare)
		{
		  	if (!$number_compare->Test($filesize)) return false;
		}
	
		return true;
	}

	protected function _IsPruned($dir, $entry)
	{
		if (!count($this->m_ignoreDir)) return false;
	
		foreach ($this->m_ignoreDir as $args)
		{
		  	$regex = $args[1];
		  	if (preg_match($regex, $entry)) return true;
		}
	
		return false;
	}

	protected function _IsDiscarded($dir, $entry)
	{
		if (!count($this->m_ignoreFile)) return false;
	
		foreach ($this->m_ignoreFile as $args)
		{
		  	$regex = $args[1];
		  	if (preg_match($regex, $entry)) return true;
		}
	
		return false;
	}

	protected function _ExecOk($dir, $entry)
	{
		if (!count($this->m_execs)) return true;
	
		foreach ($this->m_execs as $exec)
		{
		  	if (!call_user_func_array($exec, array($dir, $entry))) return false;
		}
	
		return true;
	}
	
	protected function _ToRegexString($string)
	{
		if ( ($string{0} == '/' && $string{strlen($string) - 1} == '/') ||
             ($string{0} == '/' && $string{strlen($string) - 2} == '/' 
                && $string{strlen($string) - 1} == 'i') )
	    {
	      	return $string;
	    }
	    
	    $first_byte 		= true;
		$escaping 			= false;
		$in_curlies 		= 0;
		$regex 				= '';
		$strict_leading_dot		= true;
		$strict_wildcard_slash	= true;
		for ($i = 0; $i < strlen($string); $i++)
		{
			$car = $string[$i];
			if ($first_byte)
			{
				if ($strict_leading_dot && $car != '.')
				{
				  	$regex .= '(?=[^\.])';
				}
	
				$first_byte = false;
			}

			if ($car == '/')
			{
				$first_byte = true;
			}

			if ($car == '.' || $car == '(' || $car == ')' || $car == '|' || $car == '+' || $car == '^' || $car == '$')
			{
				$regex .= "\\$car";
			}
			else if ($car == '*')
			{
				$regex .= ($escaping ? "\\*" : ($strict_wildcard_slash ? "[^/]*" : ".*"));
			}
			else if ($car == '?')
			{
				$regex .= ($escaping ? "\\?" : ($strict_wildcard_slash ? "[^/]" : "."));
			}
			else if ($car == '{')
			{
				$regex .= ($escaping ? "\\{" : "(");
				if (!$escaping) ++$in_curlies;
			}
			else if ($car == '}' && $in_curlies)
			{
				$regex .= ($escaping ? "}" : ")");
				if (!$escaping) --$in_curlies;
			}
			else if ($car == ',' && $in_curlies)
			{
				$regex .= ($escaping ? "," : "|");
			}
			else if ($car == "\\")
			{
				if ($escaping)
				{
				  	$regex .= "\\\\";
				  	$escaping = false;
				}
				else
				{
				  	$escaping = true;
				}
	
				continue;
			}
			else
			{
				$regex .= $car;
				$escaping = false;
			}
			$escaping = false;
		}

		return "#^$regex$#";
	}
	
	protected function _ArgsToArray($arg_list, $not = false)
	{
		$list = array();

	    for ($i = 0; $i < count($arg_list); $i++)
	    {
	      	if (is_array($arg_list[$i]))
	      	{
		        foreach ($arg_list[$i] as $arg)
		        {
		          	$list[] = array($not, $this->_ToRegexString($arg));
		        }
	      	}
	      	else
	      	{
	        	$list[] = array($not, $this->_ToRegexString($arg_list[$i]));
	      	}
	    }
	
	    return $list;
	}
}








/**
 * Numeric comparisons.
 *
 * CNumberCompare compiles a simple comparison to an anonymous
 * subroutine, which you can call with a value to be tested again.

 * Now this would be very pointless, if CNumberCompare didn't understand
 * magnitudes.

 * The target value may use magnitudes of kilobytes (k, ki),
 * megabytes (m, mi), or gigabytes (g, gi).  Those suffixed
 * with an i use the appropriate 2**n version in accordance with the
 * IEC standard: http://physics.nist.gov/cuu/Units/binary.html
 *
 * based on perl Number::Compare module.
 */
class CNumberCompare extends CObject
{
  	protected $m_test = '';

  	function __construct($test)
  	{
  		parent::__construct();
    	$this->m_test = $test;
  	}

  	function __destruct()
  	{
  		parent::__destruct();
  	}
  	
  	function Test($number)
  	{
	    if (!preg_match('{^([<>]=?)?(.*?)([kmg]i?)?$}i', $this->m_test, $matches))
	    {
	      throw new CException(sprintf('don\'t understand "%s" as a test.', $this->m_test));
	    }
	
	    $target 	= array_key_exists(2, $matches) ? $matches[2] : '';
	    $magnitude 	= array_key_exists(3, $matches) ? $matches[3] : '';
	    if (strtolower($magnitude) == 'k')  $target *=           1000;
	    if (strtolower($magnitude) == 'ki') $target *=           1024;
	    if (strtolower($magnitude) == 'm')  $target *=        1000000;
	    if (strtolower($magnitude) == 'mi') $target *=      1024*1024;
	    if (strtolower($magnitude) == 'g')  $target *=     1000000000;
	    if (strtolower($magnitude) == 'gi') $target *= 1024*1024*1024;
	
	    $comparison = array_key_exists(1, $matches) ? $matches[1] : '==';
	    if ($comparison == '==' || $comparison == '')
	    {
	      	return ($number == $target);
	    }
	    else if ($comparison == '>')
	    {
	      	return ($number > $target);
	    }
	    else if ($comparison == '>=')
	    {
	      	return ($number >= $target);
	    }
	    else if ($comparison == '<')
	    {
	      	return ($number < $target);
	    }
	    else if ($comparison == '<=')
	    {
	      	return ($number <= $target);
	    }
	
	    return false;
  	}
}

?>